package com.lipaluma.field.mapper.factory;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.joda.time.DateTime;

import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.field.mapper.FieldMapper;
import com.lipaluma.field.mapper.single.impl.BigDecimalFieldMapper;
import com.lipaluma.field.mapper.single.impl.BooleanFieldMapper;
import com.lipaluma.field.mapper.single.impl.CollectionFieldMapper;
import com.lipaluma.field.mapper.single.impl.DateTimeFieldMapper;
import com.lipaluma.field.mapper.single.impl.DecimalFieldMapper;
import com.lipaluma.field.mapper.single.impl.EnumFieldMapper;
import com.lipaluma.field.mapper.single.impl.IntegerFieldMapper;
import com.lipaluma.field.mapper.single.impl.ObjectFieldMapper;
import com.lipaluma.field.mapper.single.impl.StringFieldMapper;

/**
 * A static class that provides utility methods that allow instantiating a column by reflection.
 * 
 * @author mario.soares
 */
public class FieldMapperFactory {
	private static Map<Class<?>, Class<? extends FieldMapper>> mapFieldMapperType = new HashMap<Class<?>, Class<? extends FieldMapper>>();
	
	public static Class<? extends FieldMapper> defaultFieldMapperForCollection= CollectionFieldMapper.class;
	public static Class<? extends FieldMapper> defaultFieldMapperForArray = CollectionFieldMapper.class;

	static {
		mapFieldMapperType.put(String.class, StringFieldMapper.class);
		mapFieldMapperType.put(Float.class, DecimalFieldMapper.class);
		mapFieldMapperType.put(float.class, DecimalFieldMapper.class);
		mapFieldMapperType.put(Double.class, DecimalFieldMapper.class);
		mapFieldMapperType.put(double.class, DecimalFieldMapper.class);
		mapFieldMapperType.put(Short.class, IntegerFieldMapper.class);
		mapFieldMapperType.put(short.class, IntegerFieldMapper.class);
		mapFieldMapperType.put(Integer.class, IntegerFieldMapper.class);
		mapFieldMapperType.put(int.class, IntegerFieldMapper.class);
		mapFieldMapperType.put(Long.class, IntegerFieldMapper.class);
		mapFieldMapperType.put(long.class, IntegerFieldMapper.class);
		mapFieldMapperType.put(Boolean.class, BooleanFieldMapper.class);
		mapFieldMapperType.put(boolean.class, BooleanFieldMapper.class);
		mapFieldMapperType.put(Date.class, DateTimeFieldMapper.class);
		mapFieldMapperType.put(DateTime.class, DateTimeFieldMapper.class);
		mapFieldMapperType.put(BigDecimal.class, BigDecimalFieldMapper.class);
	}

	/**
	 * create a MapColumn by instantiate clazz
	 * @param clazz the column class to instantiate
	 * @return a column of type clazz
	 */
	public static FieldMapper instantiateFieldMapper(Class<? extends FieldMapper> clazz) {
		try {
			return clazz.newInstance();
		} catch (InstantiationException ex) {
			throw new CsvParserException("The class "+clazz+" can't be instanciated.", ex);
		} catch (IllegalAccessException ex) {
			throw new CsvParserException("The class "+clazz+" is not accessible.", ex);
		}
	}
	
	/**
	 * create a column by instantiate clazzname
	 * @param clazzname the column class to instantiate
	 * @return a column of type clazzname
	 */
	public static FieldMapper instantiateFieldMapper(String clazzname) {
		try {
			return (FieldMapper)Class.forName(clazzname).newInstance();
		} catch (ClassCastException ex) {
			throw new CsvParserException("The class "+clazzname+" is not a column.", ex);
		} catch (InstantiationException ex) {
			throw new CsvParserException("The class "+clazzname+" can't be instanciated.", ex);
		} catch (IllegalAccessException ex) {
			throw new CsvParserException("The class "+clazzname+" is not accessible.", ex);
		} catch (ClassNotFoundException ex) {
			throw new CsvParserException("The class "+clazzname+" is not found.", ex);
		}
	}

	/**
	 * define the default column for Collection type
	 * 
	 * @param columnClass column class that will be assigned to the Collection type by default
	 */
	public static void defineDefaultFieldMapperForCollection(Class<? extends FieldMapper> columnClass) {
		defaultFieldMapperForCollection = columnClass;
	}

	/**
	 * define the default column for Array type
	 * 
	 * @param columnClass column class that will be assigned to the array type by default
	 */
	public static void defineDefaultFieldMapperForArray(Class<? extends FieldMapper> columnClass) {
		defaultFieldMapperForArray= columnClass;
	}

	/**
	 * add a new type with his column who maps the csv especially for that type.
	 * 
	 * @param name the name of the type upper case
	 * @param columnClass class of the column
	 */
	public static void setFieldMapperForType(Class<?> clazz, Class<? extends FieldMapper> columnClass) {
		mapFieldMapperType.put(clazz, columnClass);
	}
	
	/**
	 * returns the class defined by the type name
	 * 
	 * @param name the name of the type (upper case)
	 * @return the column class corresponding
	 */
	public static Class<? extends FieldMapper> retrieveFieldMapperClassForType(Class<?> clazz) {
		Class<? extends FieldMapper> fieldMapperClass = mapFieldMapperType.get(clazz);
		if(fieldMapperClass == null) {
			if(clazz.isArray())
				fieldMapperClass = defaultFieldMapperForArray;
			else if(Collection.class.isAssignableFrom(clazz))
				fieldMapperClass = defaultFieldMapperForCollection;
			else if(clazz.isEnum())
				return EnumFieldMapper.class;
			else
				return ObjectFieldMapper.class;
		}
		return fieldMapperClass;
	}

	/**
	 * verify if a column exists for the type name
	 * 
	 * @param name the name of the type (upper case)
	 * @return true if exists
	 */
	public static boolean isFieldMapperExistsForType(Class<?> clazz) {
		return mapFieldMapperType.containsKey(clazz) || clazz.isEnum() ||
				clazz.isArray() || Collection.class.isAssignableFrom(clazz);
	}
	
	/**
	 * instantiate and returns the column class for the type name defined
	 * 
	 * @param name the name of the type (upper case)
	 * @return a new instance of the column class corresponding
	 */
	public static <T> FieldMapper createFieldMapperForType(Class<T> clazz) {
//		String name = clazz.getSimpleName();
//		if(!isFieldMapperExistsForType(clazz))
//			throw new CsvParserException("can't instantiate FieldMapper for class "+name+".");
		Class<? extends FieldMapper> clazzColumn = retrieveFieldMapperClassForType(clazz);
		return instantiateFieldMapper(clazzColumn);
	}

}
